package no.uib.kwikktaim.utils;


/*
 * Copyright (C) 2003 Erland Isaksson (erland_i@hotmail.com)'
 *
 * Modified by Tobias.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

import javax.media.*;
import javax.media.control.TrackControl;
import javax.media.datasink.DataSinkErrorEvent;
import javax.media.datasink.DataSinkEvent;
import javax.media.datasink.DataSinkListener;
import javax.media.datasink.EndOfStreamEvent;
import javax.media.format.AudioFormat;
import javax.media.format.VideoFormat;
import javax.media.protocol.*;
import javax.media.util.BufferToImage;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Vector;

public class MovieThumbnail implements ControllerListener, DataSinkListener {
	/*** Logging instance */
	private static Object sync = new Object();
	private Processor p;
	private Object waitSync = new Object();
	private Object waitEndSync = new Object();
	private boolean stateTransitionOK = true;
	private long noOfFrames;
	private BufferedImage thumbnail;
	private static ArrayList<Image> thumbnails = new ArrayList<Image>();
	private boolean bGenerateThumbnail;
	private int frameInterval;
	private int noOfColumns;
	private int noOfRows;

	/***
	 * This DataSourceHandler class reads from a DataSource and display
	 * information of each frame of data received.
	 */
	class DataSourceHandler implements DataSink, BufferTransferHandler {
		DataSource source;
		PullBufferStream pullStrms[] = null;
		PushBufferStream pushStrms[] = null;

		// Data sink listeners.
		private Vector listeners = new Vector(1);

		// Stored all the streams that are not yet finished (i.e. EOM
		// has not been received.
		SourceStream unfinishedStrms[] = null;

		// Loop threads to pull data from a PullBufferDataSource.
		// There is one thread per each PullSourceStream.
		Loop loops[] = null;

		Buffer readBuffer;
		private int thumbnailWidth;
		private int thumbnailHeight;
		private int currentRow;
		private int currentColumn;


		/***
		 * A thread class to implement a processing loop.
		 * This loop reads data from a PullBufferDataSource.
		 */
		class Loop extends Thread {

			DataSourceHandler handler;
			PullBufferStream stream;
			boolean paused = true;
			boolean killed = false;

			public Loop(DataSourceHandler handler, PullBufferStream stream) {
				this.handler = handler;
				this.stream = stream;
				start();
			}

			public synchronized void restart() {
				paused = false;
				notify();
			}

			/***
			 * This is the correct way to pause a thread; unlike suspend.
			 */
			public synchronized void pause() {
				paused = true;
			}

			/***
			 * This is the correct way to kill a thread; unlike stop.
			 */
			public synchronized void kill() {
				killed = true;
				notify();
			}

			/***
			 * This is the processing loop to pull data from a
			 * PullBufferDataSource.
			 */
			public void run() {
				while (!killed) {
					try {
						while (paused && !killed) {
							wait();
						}
					} catch (InterruptedException e) {
					}

					if (!killed) {
						boolean done = handler.readPullData(stream);
						if (done)
							pause();
					}
				}
			}
		}

		/***
		 * Sets the media source this <code>MediaHandler</code>
		 * should use to obtain content.
		 */
		public void setSource(DataSource source) throws IncompatibleSourceException {

			// Different types of DataSources need to handled differently.
			if (source instanceof PushBufferDataSource) {

				pushStrms = ((PushBufferDataSource) source).getStreams();
				unfinishedStrms = new SourceStream[pushStrms.length];

				// Set the transfer handler to receive pushed data from
				// the push DataSource.
				for (int i = 0; i < pushStrms.length; i++) {
					pushStrms[i].setTransferHandler(this);
					unfinishedStrms[i] = pushStrms[i];
				}


			} else if (source instanceof PullBufferDataSource) {

				pullStrms = ((PullBufferDataSource) source).getStreams();
				unfinishedStrms = new SourceStream[pullStrms.length];

				// For pull data sources, we'll start a thread per
				// stream to pull data from the source.
				loops = new Loop[pullStrms.length];
				for (int i = 0; i < pullStrms.length; i++) {
					loops[i] = new Loop(this, pullStrms[i]);
					unfinishedStrms[i] = pullStrms[i];
				}

			} else {

				// This handler only handles push or pull buffer datasource.
				throw new IncompatibleSourceException();

			}

			this.source = source;
			readBuffer = new Buffer();
		}


		/***
		 * For completeness, DataSink's require this method.
		 * But we don't need it.
		 */
		public void setOutputLocator(MediaLocator ml) {
		}


		public MediaLocator getOutputLocator() {
			return null;
		}


		public String getContentType() {
			return source.getContentType();
		}


		/***
		 * Our DataSink does not need to be opened.
		 */
		public void open() {
		}


		public void start() {
			try {
				source.start();
			} catch (IOException e) {
				e.printStackTrace();
			}

			// Start the processing loop if we are dealing with a
			// PullBufferDataSource.
			if (loops != null) {
				for (int i = 0; i < loops.length; i++)
					loops[i].restart();
			}
		}


		public void stop() {
			try {
				source.stop();
			} catch (IOException e) {
				e.printStackTrace();
			}

			// Start the processing loop if we are dealing with a
			// PullBufferDataSource.
			if (loops != null) {
				for (int i = 0; i < loops.length; i++)
					loops[i].pause();
			}
		}


		public void close() {
			stop();
			if (loops != null) {
				for (int i = 0; i < loops.length; i++)
					loops[i].kill();
			}
		}


		public void addDataSinkListener(DataSinkListener dsl) {
			if (dsl != null)
				if (!listeners.contains(dsl))
					listeners.addElement(dsl);
		}


		public void removeDataSinkListener(DataSinkListener dsl) {
			if (dsl != null)
				listeners.removeElement(dsl);
		}


		protected void sendEvent(DataSinkEvent event) {
			if (!listeners.isEmpty()) {
				synchronized (listeners) {
					Enumeration list = listeners.elements();
					while (list.hasMoreElements()) {
						DataSinkListener listener =
							(DataSinkListener) list.nextElement();
						listener.dataSinkUpdate(event);
					}
				}
			}
		}


		/***
		 * This will get called when there's data pushed from the
		 * PushBufferDataSource.
		 */
		public void transferData(PushBufferStream stream) {

			try {
				stream.read(readBuffer);
			} catch (IOException e) {
				System.out.println("Error while reading JMF stream");
				System.out.println(e);
				sendEvent(new DataSinkErrorEvent(this, e.getMessage()));
				return;
			}

			//printDataInfo(readBuffer);
			if (!readBuffer.isEOM()) {
				if (bGenerateThumbnail && (readBuffer.getSequenceNumber() - 1) % frameInterval == 0) {
					addToJPEG(readBuffer);
				}
			} else {
				noOfFrames = readBuffer.getSequenceNumber();
			}

			// Check to see if we are done with all the streams.
			if (readBuffer.isEOM() && checkDone(stream)) {
				sendEvent(new EndOfStreamEvent(this));
			}
		}


		/***
		 * This is called from the Loop thread to pull data from
		 * the PullBufferStream.
		 */
		public boolean readPullData(PullBufferStream stream) {
			try {
				stream.read(readBuffer);
			} catch (IOException e) {
				System.out.println("Error while reading JMF stream");
				e.printStackTrace();
				return true;
			}

			//printDataInfo(readBuffer);

			//writeJPEG(readBuffer);
			if (readBuffer.isEOM()) {
				// Check to see if we are done with all the streams.
				if (checkDone(stream)) {
					close();
				}
				return true;
			}
			return false;
		}


		/***
		 * Check to see if all the streams are processed.
		 */
		public boolean checkDone(SourceStream strm) {
			boolean done = true;

			for (int i = 0; i < unfinishedStrms.length; i++) {
				if (strm == unfinishedStrms[i])
					unfinishedStrms[i] = null;
				else if (unfinishedStrms[i] != null) {
					// There's at least one stream that's not done.
					done = false;
				}
			}
			return done;
		}


		void printDataInfo(Buffer buffer) {
			if (buffer.getFormat() instanceof AudioFormat)
				System.out.println("Read audio data:");
			else
				System.out.println("Read video data:");
			System.out.println("  Time stamp: " + buffer.getTimeStamp());
			System.out.println("  Sequence #: " + buffer.getSequenceNumber());
			System.out.println("  Data length: " + buffer.getLength());

			if (buffer.isEOM())
				System.out.println("  Got EOM!");
		}

		public Object[] getControls() {
			return new Object[0];
		}

		public Object getControl(String name) {
			return null;
		}

		void addToJPEG(Buffer buffer) {
			BufferToImage stopBuffer = new BufferToImage((VideoFormat) buffer.getFormat());
			Image stopImage = stopBuffer.createImage(buffer);
			if (stopImage != null) {
				if (thumbnail == null) {
					currentRow = 0;
					currentColumn = 0;

					thumbnail = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
				} else {
					currentColumn++;
					while (currentColumn >= noOfColumns) {
						currentColumn -= noOfColumns;
						currentRow++;
					}
				}
				thumbnails.add(stopImage);

			}
		}
	}

	public MovieThumbnail(int noOfCols, int noOfRows) {
		this.noOfColumns = noOfCols;
		this.noOfRows = noOfRows;
	}

	public BufferedImage create(URL movie, int width, int height,ImageFilterContainerInterface filters) {
		synchronized (sync) {
			MediaLocator ml;
			ml = new MediaLocator(movie);
			if (movie == null) {
				return null;
			}
			DataSource ds = null;

			try {
				ds = Manager.createDataSource(ml);
				long noOfFrames = getNumberOfFrames(ds);
				if(noOfFrames>0) {
					ds = Manager.createDataSource(ml);
					return createThumbnail(ds, (int) (noOfFrames / (noOfColumns * noOfRows)), noOfColumns, noOfRows, width, height);
				}
			} catch (Exception e) {
				System.out.println("Error when creating movie thumbnail for "+(movie!=null?movie.getFile():"null"));
				e.printStackTrace();
			}
			return null;
		}
	}

	private long getNumberOfFrames(DataSource ds) {
		noOfFrames = 0;
		bGenerateThumbnail = false;
		init(ds);
		return noOfFrames;
	}

	private BufferedImage createThumbnail(DataSource ds, int frameInterval, int noOfColumns, int noOfRows, int width, int height) {
		noOfFrames = 0;
		thumbnail = null;
		bGenerateThumbnail = true;
		this.frameInterval = frameInterval;
		init(ds);
		return thumbnail;
	}

	private boolean init(DataSource ds) {

		try {
			p = Manager.createProcessor(ds);
		} catch (IOException e) {
			System.out.println("Error when creating JMF processor");
			e.printStackTrace();
			return false;
		} catch (NoProcessorException e) {
			System.out.println("Error when creating JMF processor");
			e.printStackTrace();
			return false;
		}

		p.addControllerListener(this);

		p.configure();
		if (!waitForState(Processor.Configured)) {
			System.out.println("Failed to configure the processor");
			return false;
		}

		// Get the raw output from the processor.
		p.setContentDescriptor(new ContentDescriptor(ContentDescriptor.RAW));

		// Change to RGB Video format output and disable audio tracks
		TrackControl tc[] = p.getTrackControls();
		for (int i = 0; i < tc.length; i++) {
			if (tc[i].getFormat() instanceof VideoFormat)
				tc[i].setFormat(new VideoFormat(VideoFormat.RGB));
			else
				tc[i].setEnabled(false);
		}

		p.realize();
		if (!waitForState(Processor.Realized)) {
			System.out.println("Failed to realize the JMF processor");
			return false;
		}

		// Get the output DataSource from the processor and
		// hook it up to the DataSourceHandler.
		DataSource ods = p.getDataOutput();
		DataSourceHandler handler = new DataSourceHandler();

		try {
			handler.setSource(ods);
		} catch (IncompatibleSourceException e) {
			System.out.println("Cannot handle the output DataSource from the processor: " + ods);
			e.printStackTrace();
			return false;
		}

		handler.addDataSinkListener(this);
		handler.start();

		// Prefetch the processor.
		p.prefetch();
		if (!waitForState(Processor.Prefetched)) {
			System.out.println("Failed to prefetch the processor");
			return false;
		}

		// Start the processor.
		p.start();
		try {
			synchronized (waitEndSync) {
				waitEndSync.wait();
			}
		} catch (InterruptedException e) {
			System.out.println("JMF processor interupted");
			e.printStackTrace();
			return false;
		}
		return true;
	}

	/***
	 * Block until the processor has transitioned to the given state.
	 * Return false if the transition failed.
	 */
	boolean waitForState(int state) {
		synchronized (waitSync) {
			try {
				while (p.getState() < state && stateTransitionOK)
					waitSync.wait();
			} catch (Exception e) {
			}
		}
		return stateTransitionOK;
	}

	/***
	 * Change the plugin list to disable the default RawBufferMux
	 * thus allowing the RawSyncBufferMux to be used.
	 * This is a handy trick.  You wouldn't know this, would you? :)
	 */
	void enableSyncMux() {
		Vector muxes = PlugInManager.getPlugInList(null, null,
				PlugInManager.MULTIPLEXER);
		for (int i = 0; i < muxes.size(); i++) {
			String cname = (String) muxes.elementAt(i);
			if (cname.equals("com.sun.media.multiplexer.RawBufferMux")) {
				muxes.removeElementAt(i);
				break;
			}
		}
		PlugInManager.setPlugInList(muxes, PlugInManager.MULTIPLEXER);
	}


	/***
	 * Controller Listener.
	 */
	public void controllerUpdate(ControllerEvent evt) {

		if (evt instanceof ConfigureCompleteEvent ||
				evt instanceof RealizeCompleteEvent ||
				evt instanceof PrefetchCompleteEvent) {
			synchronized (waitSync) {
				stateTransitionOK = true;
				waitSync.notifyAll();
			}
		} else if (evt instanceof ResourceUnavailableEvent) {
			synchronized (waitSync) {
				stateTransitionOK = false;
				waitSync.notifyAll();
			}
		} else if (evt instanceof EndOfMediaEvent) {
			p.close();
			synchronized (waitEndSync) {
				waitEndSync.notifyAll();
			}
		} else if (evt instanceof SizeChangeEvent) {
		}
	}


	/***
	 * DataSink Listener
	 */
	public void dataSinkUpdate(DataSinkEvent evt) {

		if (evt instanceof EndOfStreamEvent) {
			evt.getSourceDataSink().close();
		}
	}

	/**
	 * Method generating image from video-file.
	 * @param url - url of the movie
	 * @return image containing several images
	 */
	public static ArrayList<Image> createImageFromURL(URL url) {
		MovieThumbnail me = new MovieThumbnail(6, 4);
		BufferedImage image = me.create(url, 600,480,null);
		if (image != null) {
			return thumbnails;
		} else {
			System.out.println("No image generated");
			return null;
		}
	}
}
